/* * COMSAT * Copyright (c) 2015-2016, Parallel Universe Software Co. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation * * or (per the licensee's choosing) * * under the terms of the GNU Lesser General Public License version 3.0 * as published by the Free Software Foundation. */ package co.paralleluniverse.comsat.webactors.netty; import co.paralleluniverse.actors.ActorRef; import co.paralleluniverse.comsat.webactors.Cookie; import co.paralleluniverse.comsat.webactors.HttpRequest; import co.paralleluniverse.comsat.webactors.HttpResponse; import co.paralleluniverse.comsat.webactors.WebMessage; import com.google.common.collect.*; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; import java.nio.charset.UnsupportedCharsetException; import java.util.*; import static io.netty.handler.codec.http.HttpHeaders.Names.*; /** * @author circlespainter */ public final class NettyHttpRequest extends HttpRequest { public static final String CHARSET_MARKER_STRING = "charset="; final ActorRef<? super HttpResponse> actorRef; final FullHttpRequest req; final ChannelHandlerContext ctx; final String sessionId; private static final Set<io.netty.handler.codec.http.cookie.Cookie> EMPTY_SET = new HashSet<>(); private final ByteBuf reqContent; private InetSocketAddress sourceAddress; private ImmutableMultimap<String, String> params; private URI uri; private Collection<Cookie> cookies; private ListMultimap<String, String> heads; private ByteBuffer byteBufferBody; private String stringBody; private Charset encoding; private String contentType; public NettyHttpRequest(ActorRef<? super HttpResponse> actorRef, ChannelHandlerContext ctx, FullHttpRequest req, String sessionId) { this.actorRef = actorRef; this.ctx = ctx; this.req = req; this.sessionId = sessionId; reqContent = Unpooled.copiedBuffer(req.content()); } @Override public final String getSourceHost() { fillSourceAddress(); return sourceAddress != null ? sourceAddress.getHostString() : null; } @Override public final int getSourcePort() { fillSourceAddress(); return sourceAddress != null ? sourceAddress.getPort() : -1; } private void fillSourceAddress() { final SocketAddress remoteAddress = ctx.channel().remoteAddress(); if (sourceAddress == null && remoteAddress instanceof InetSocketAddress) { sourceAddress = (InetSocketAddress) remoteAddress; } } @Override public final Multimap<String, String> getParameters() { QueryStringDecoder queryStringDecoder; if (params == null) { queryStringDecoder = new QueryStringDecoder(req.getUri()); final ImmutableMultimap.Builder<String, String> builder = ImmutableMultimap.builder(); final Map<String, List<String>> parameters = queryStringDecoder.parameters(); for (final String k : parameters.keySet()) builder.putAll(k, parameters.get(k)); params = builder.build(); } return params; } @Override public final Map<String, Object> getAttributes() { return ImmutableMap.of(); // No attributes in Netty; Guava's impl. will return a pre-built instance } @Override public final String getScheme() { initUri(); return uri.getScheme(); } private void initUri() { if (uri == null) { try { uri = new URI(req.getUri()); } catch (final URISyntaxException e) { throw new RuntimeException(e); } } } @Override public final String getMethod() { return req.getMethod().name(); } @Override public final String getPathInfo() { initUri(); return uri.getPath(); } @Override public final String getContextPath() { return "/"; // Context path makes sense only for servlets } @Override public final String getQueryString() { initUri(); return uri.getQuery(); } @Override public final String getRequestURI() { return req.getUri(); } @Override public final String getServerName() { initUri(); return uri.getHost(); } @Override public final int getServerPort() { initUri(); return uri.getPort(); } @SuppressWarnings("unchecked") @Override public final ActorRef<WebMessage> getFrom() { return (ActorRef<WebMessage>) actorRef; } @Override public final ListMultimap<String, String> getHeaders() { if (heads == null) { heads = extractHeaders(req.headers()); } return heads; } @Override public final Collection<Cookie> getCookies() { if (cookies == null) { final ImmutableList.Builder<Cookie> builder = ImmutableList.builder(); for (io.netty.handler.codec.http.cookie.Cookie c : getNettyCookies(req)) { builder.add( Cookie.cookie(c.name(), c.value()) .setDomain(c.domain()) .setPath(c.path()) .setHttpOnly(c.isHttpOnly()) .setMaxAge((int) c.maxAge()) .setSecure(c.isSecure()) .build() ); } cookies = builder.build(); } return cookies; } static Set<io.netty.handler.codec.http.cookie.Cookie> getNettyCookies(FullHttpRequest req) { final HttpHeaders heads = req.headers(); final String head = heads != null ? heads.get(HttpHeaders.Names.COOKIE) : null; if (head != null) return ServerCookieDecoder.LAX.decode(head); else return EMPTY_SET; } @Override public final int getContentLength() { final String stringBody = getStringBody(); if (stringBody != null) return stringBody.length(); final ByteBuffer bufferBody = getByteBufferBody(); if (bufferBody != null) return bufferBody.remaining(); return 0; } @Override public final Charset getCharacterEncoding() { if (encoding == null) encoding = extractCharacterEncoding(getHeaders()); return encoding; } @Override public final String getContentType() { if (contentType == null) { getHeaders(); if (heads != null) { final List<String> cts = heads.get(CONTENT_TYPE); if (cts != null && cts.size() > 0) contentType = cts.get(0); } } return null; } @Override public final String getStringBody() { if (stringBody == null) { if (byteBufferBody != null) return null; decodeStringBody(); } return stringBody; } @Override public final ByteBuffer getByteBufferBody() { if (byteBufferBody == null) { if (stringBody != null) return null; if (reqContent != null) byteBufferBody = reqContent.nioBuffer(); } return byteBufferBody; } public final String getSessionId() { return sessionId; } public ChannelHandlerContext getContext() { return ctx; } public FullHttpRequest getRequest() { return req; } private String decodeStringBody() { if (reqContent != null) { try { stringBody = getCharacterEncodingOrDefault() .newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT) .decode(reqContent.nioBuffer()) .toString(); } catch (CharacterCodingException ignored) { } } return stringBody; } Charset getCharacterEncodingOrDefault() { return getCharacterEncodingOrDefault(getCharacterEncoding()); } static Charset extractCharacterEncodingOrDefault(HttpHeaders headers) { return getCharacterEncodingOrDefault(extractCharacterEncoding(extractHeaders(headers))); } static Charset extractCharacterEncoding(ListMultimap<String, String> heads) { if (heads != null) { final List<String> cts = heads.get(CONTENT_TYPE); if (cts != null && cts.size() > 0) { final String ct = cts.get(0).trim().toLowerCase(); if (ct.contains(CHARSET_MARKER_STRING)) { try { return Charset.forName(ct.substring(ct.indexOf(CHARSET_MARKER_STRING) + CHARSET_MARKER_STRING.length()).trim()); } catch (UnsupportedCharsetException ignored) { } } } } return null; } static ImmutableListMultimap<String, String> extractHeaders(HttpHeaders headers) { if (headers != null) { final ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder(); for (final String n : headers.names()) // Normalize header names by their conversion to lower case builder.putAll(n.toLowerCase(Locale.ENGLISH), headers.getAll(n)); return builder.build(); } return null; } private static Charset getCharacterEncodingOrDefault(Charset characterEncoding) { if (characterEncoding == null) return Charset.defaultCharset(); return characterEncoding; } }